# Fibonacci-CFS Attractor with Native Plotly Animation for Quarto
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
class FibonacciCFSAttractorPlotly:
def __init__(self, num_ribbons=8, steps=1200, dt=0.018):
self.num_ribbons = num_ribbons
self.steps = steps
self.dt = dt
self.phi = (1 + np.sqrt(5)) / 2 # Golden ratio
self.trajectories = []
def advanced_dynamics(self, x, y, z, t, ribbon_offset):
"""Enhanced dynamics for smooth, organic flow"""
r = np.sqrt(x**2 + y**2 + z**2)
# Multi-level Fibonacci scaling
level1 = int(np.log(r + 1) / np.log(self.phi)) if r > 0 else 0
level2 = int(t * 0.08) % 12
# Enhanced coherence with multiple harmonics
coherence = (self.phi**(-level1 * 0.05) *
(1 + 0.25 * np.sin(level1 * 0.3 + t * 0.08)) *
(1 + 0.15 * np.cos(level2 * 0.2 + ribbon_offset)))
# Multi-scale power law coupling
coupling = (1.0 * (r + 0.5)**(-0.28) *
(1 + 0.18 * np.cos(0.06 * t + ribbon_offset)) *
(1 + 0.12 * np.sin(0.04 * t)))
# Complex spiral feedback
theta = np.arctan2(y, x) + 0.05 * t + ribbon_offset
spiral_x = (0.4 * np.cos(theta + 0.08 * z) +
0.15 * np.cos(1.8 * theta + 0.03 * t))
spiral_y = (0.4 * np.sin(theta + 0.08 * z) +
0.15 * np.sin(1.8 * theta + 0.03 * t))
spiral_z = (0.18 * np.sin(0.12 * t + ribbon_offset) +
0.08 * np.cos(0.06 * t + 1.5 * ribbon_offset))
# Attractor dynamics
dx = coupling * (self.phi * y - 0.75 * x + 0.08 * z) + spiral_x
dy = coupling * (coherence * x - y - 0.04 * x * z) + spiral_y
dz = coupling * (0.08 * x * y - 0.35 * coherence * z + 0.03 * x) + spiral_z
return dx, dy, dz
def generate_trajectory(self, ribbon_index):
"""Generate trajectory with organic initial conditions"""
ribbon_offset = 2 * np.pi * ribbon_index / self.num_ribbons
# Varied initial conditions for organic start
r0 = 0.6 + 0.4 * np.sin(ribbon_offset + 0.5)
theta0 = ribbon_offset + 0.3 * np.sin(1.5 * ribbon_offset)
x = r0 * np.cos(theta0)
y = r0 * np.sin(theta0)
z = 0.4 * np.sin(ribbon_offset + 0.8) + 0.15 * ribbon_index / self.num_ribbons
trajectory = []
for step in range(self.steps):
t = step * self.dt
dx, dy, dz = self.advanced_dynamics(x, y, z, t, ribbon_offset)
x += dx * self.dt
y += dy * self.dt
z += dz * self.dt
trajectory.append([x, y, z])
return np.array(trajectory)
def generate_all_trajectories(self):
"""Generate all trajectories"""
self.trajectories = []
for i in range(self.num_ribbons):
traj = self.generate_trajectory(i)
self.trajectories.append(traj)
return self.trajectories
def create_animated_figure(self):
"""Create animated figure with native Plotly animation"""
print("Generating trajectories...")
self.generate_all_trajectories()
# Create color palette
colors = px.colors.qualitative.Set3[:self.num_ribbons]
# Prepare animation frames
frames = []
animation_steps = 150 # Number of animation frames
trail_length = 120
for frame_idx in range(animation_steps):
frame_data = []
max_points = min(frame_idx * 8 + 100, self.steps)
# Add ribbon trails for this frame
for ribbon_idx, (traj, color) in enumerate(zip(self.trajectories, colors)):
if max_points > trail_length:
start_idx = max_points - trail_length
end_idx = max_points
trail_data = traj[start_idx:end_idx]
# Create multiple segments with fading effect
segment_length = max(1, len(trail_data) // 6)
for seg in range(6):
seg_start = seg * segment_length
seg_end = min((seg + 1) * segment_length, len(trail_data))
if seg_end > seg_start:
segment_data = trail_data[seg_start:seg_end]
# Fading parameters
fade_factor = (seg + 1) / 6.0
opacity = 0.2 + 0.6 * fade_factor
line_width = 2 + 4 * fade_factor
# Add segment trace
frame_data.append(
go.Scatter3d(
x=segment_data[:, 0],
y=segment_data[:, 1],
z=segment_data[:, 2],
mode='lines',
line=dict(color=color, width=line_width),
opacity=opacity,
showlegend=False,
name=f'Ribbon {ribbon_idx+1}'
)
)
# Add current position marker
if max_points > 0 and max_points <= len(traj):
current_point = traj[max_points-1]
frame_data.append(
go.Scatter3d(
x=[current_point[0]],
y=[current_point[1]],
z=[current_point[2]],
mode='markers',
marker=dict(size=10, color=color, opacity=1.0),
showlegend=False,
name=f'Current {ribbon_idx+1}'
)
)
# Add connection ribbons
for i in range(0, self.num_ribbons - 1, 2):
if i + 1 < len(self.trajectories) and max_points > 30:
traj1 = self.trajectories[i]
traj2 = self.trajectories[i + 1]
# Sample connection points
connection_indices = range(max(0, max_points - 80), max_points, 20)
for point_idx in connection_indices:
if point_idx < len(traj1) and point_idx < len(traj2):
frame_data.append(
go.Scatter3d(
x=[traj1[point_idx, 0], traj2[point_idx, 0]],
y=[traj1[point_idx, 1], traj2[point_idx, 1]],
z=[traj1[point_idx, 2], traj2[point_idx, 2]],
mode='lines',
line=dict(color='rgba(0, 200, 200, 0.4)', width=3),
showlegend=False,
name='Connection'
)
)
# Create frame
frames.append(go.Frame(data=frame_data, name=str(frame_idx)))
# Create initial figure with first frame data
fig = go.Figure(data=frames[0].data, frames=frames)
# Update layout
fig.update_layout(
title=dict(
text="Fibonacci-CFS Attractor: Interactive Animation",
x=0.5,
font=dict(size=18, color='#2c3e50')
),
scene=dict(
xaxis=dict(
range=[-6, 6],
showgrid=True,
gridcolor='lightgray',
title='X'
),
yaxis=dict(
range=[-6, 6],
showgrid=True,
gridcolor='lightgray',
title='Y'
),
zaxis=dict(
range=[-3, 3],
showgrid=True,
gridcolor='lightgray',
title='Z'
),
bgcolor='white',
camera=dict(
eye=dict(x=1.8, y=1.8, z=1.2),
center=dict(x=0, y=0, z=0)
),
aspectmode='cube'
),
updatemenus=[{
'type': 'buttons',
'showactive': False,
'buttons': [
{
'label': 'Play',
'method': 'animate',
'args': [None, {
'frame': {'duration': 100, 'redraw': True},
'fromcurrent': True,
'transition': {'duration': 50}
}]
},
{
'label': 'Pause',
'method': 'animate',
'args': [[None], {
'frame': {'duration': 0, 'redraw': False},
'mode': 'immediate',
'transition': {'duration': 0}
}]
},
{
'label': 'Reset',
'method': 'animate',
'args': [['0'], {
'frame': {'duration': 0, 'redraw': True},
'mode': 'immediate',
'transition': {'duration': 0}
}]
}
],
'direction': 'left',
'pad': {'r': 10, 't': 87},
'x': 0.1,
'xanchor': 'right',
'y': 0,
'yanchor': 'top'
}],
sliders=[{
'steps': [
{
'args': [[str(k)], {
'frame': {'duration': 0, 'redraw': True},
'mode': 'immediate',
'transition': {'duration': 0}
}],
'label': str(k),
'method': 'animate'
} for k in range(len(frames))
],
'active': 0,
'yanchor': 'top',
'xanchor': 'left',
'currentvalue': {
'font': {'size': 16},
'prefix': 'Frame: ',
'visible': True,
'xanchor': 'right'
},
'transition': {'duration': 0},
'x': 0.1,
'y': 0,
'len': 0.9
}],
width=900,
height=700,
margin=dict(l=0, r=0, t=50, b=100),
paper_bgcolor='white',
plot_bgcolor='white'
)
return fig
# Create and display the animation
print("Creating Fibonacci-CFS attractor animation...")
attractor = FibonacciCFSAttractorPlotly(num_ribbons=8, steps=1200, dt=0.018)
fig = attractor.create_animated_figure()
# Display the figure
fig.show()
print("\nInteractive Animation Features:")
print("✅ Play/Pause/Reset buttons")
print("✅ Frame slider for manual control")
print("✅ 3D zoom, pan, and rotate")
print("✅ Fading trail visualization")
print("✅ Connection ribbons between trajectories")
print("✅ Mathematical rigor with Fibonacci-CFS framework")
print("✅ Native Quarto/Jupyter compatibility")